// Copyright 2016 The Fuchsia Authors
// Copyright (c) 2009 Corey Tabaka
// Copyright (c) 2015 Intel Corporation
// Copyright (c) 2016 Travis Geiselbrecht
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT

#if WITH_KERNEL_PCIE

#include <inttypes.h>
#include <trace.h>
#include <zircon/types.h>

#include <arch/x86/feature.h>
#include <dev/pcie_bus_driver.h>
#include <dev/pcie_device.h>
#include <fbl/algorithm.h>
#include <fbl/ref_ptr.h>

#define LOCAL_TRACE 0

// Top-of-lower-usable-DRAM quirk.
//
// Intel processors sometimes steal a bit memory for GPU and SMM needs.  When
// they do, the BIOS/bootloader sometimes does not report these regions as
// reserved in the memory map passed to the OS, they just remove them from the
// usable RAM portion of the memory map.  If we fail to remove these regions
// from the set allocatable MMIO regions used by the PCIe bus driver, we can end
// up allocating portions of the bus containing GPU/SMM stolen memory to devices
// to use for BAR windows (this would be Very Bad).
//
// For processors which have a "TOLUD" register (top of lower usable DRAM), we
// can simply subtract out the region [0, TOLUD) from the PCIe bus driver's
// allocatable regions.  This register (on 6th gen Intel Core processors at
// least) lives in the config space for the host bridge device.  Look for it and
// subtract out the region.  If we don't find the register, and cannot be sure
// that the target we are running on does not need this special treatment, log a
// big warning so someone can come and update this code to do the right thing.
static void pcie_tolud_quirk(const fbl::RefPtr<PcieDevice>& dev) {
  // TODO(johngro): Expand this table as we add support for new
  // processors/chipsets.  Set offset to 0 if no action needs to be taken.
  static const struct {
    uint32_t match;
    uint32_t mask;
    uint16_t offset;
  } TOLUD_CHIPSET_LUT[] = {
      // QEMU's emulation of Intel Q35.   No TOLUD register that I know of.
      {.match = 0x808629c0, .mask = 0xFFFFFFFF, .offset = 0x0},
      // PIIX4
      {.match = 0x80861237, .mask = 0xFFFFFFFF, .offset = 0x0},
      // Second/Third gen core family
      {.match = 0x80860100, .mask = 0xFFFFFF00, .offset = 0xBC},
      // Intel 6th Generation Core Family (Skylake)
      {.match = 0x80861900, .mask = 0xFFFFFF00, .offset = 0xBC},

      // Intel 7th Generation Core Family (Kaby Lake)
      //
      // TODO(johngro) : Get confirmation of this.  Intel's public docs claim
      // that the DID is 0x19xx, like Skylake.  Hardware I have seen
      // (i3-7100u), as well as HW that people have talked about online
      // (i5-7500u, as well as some desktop SKUs), however, all seem to use
      // 0x59xx.
      {.match = 0x80865900, .mask = 0xFFFFFF00, .offset = 0xBC},
  };

  // only makes sense on intel hardware
  if (x86_vendor != X86_VENDOR_INTEL)
    return;

  static bool found_chipset_device = false;

  // If we have already recognized our chipset and taken appropriate action,
  // then there is nothing left for us to do.
  if (found_chipset_device)
    return;

  // If dev is nullptr, then the PCIe bus driver is about to start allocating
  // resources.  If we have not recognized the chipset we are running on yet,
  // log a big warning.  Someone needs to come into this code and add support
  // for the unrecognized chipset (even if not special action needs to be
  // taken, the quirk needs to be taught to recognize the chipset we are
  // running on).
  if (dev == nullptr) {
    if (!found_chipset_device) {
      TRACEF("WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING\n");
      TRACEF("WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING\n");
      TRACEF("PCIe TOLUD quirk was not able to identify the chipset we are running on!\n");
      TRACEF("Someone needs to teach this quirk about the new chipset!\n");
      TRACEF("WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING\n");
      TRACEF("WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING\n");
    }
    return;
  }

  // The device we are looking for will always be a BDF 00:00.0
  if (dev->bus_id() || dev->dev_id() || dev->func_id())
    return;

  // Concatenate the vendor and device ID and search our LUT to see if we
  // recognize this host bridge.
  size_t i;
  uint32_t vid_did = (static_cast<uint32_t>(dev->vendor_id()) << 16) | dev->device_id();
  for (i = 0; i < fbl::count_of(TOLUD_CHIPSET_LUT); ++i) {
    const auto& entry = TOLUD_CHIPSET_LUT[i];
    if ((vid_did & entry.mask) == entry.match)
      break;
  }

  if (i >= fbl::count_of(TOLUD_CHIPSET_LUT))
    return;

  // Looks like we recognize this chip.  Check our table to see if there is a
  // TOLUD register we should read.
  uint16_t offset = TOLUD_CHIPSET_LUT[i].offset;
  if (offset) {
    static constexpr uint32_t TOLUD_MASK = 0xFFF00000;
    auto tolud_reg = PciReg32(offset);
    uint32_t tolud_val = dev->config()->Read(tolud_reg) & TOLUD_MASK;

    // Subtract out the TOLUD region from the PCI driver's allocatable MMIO region.
    if (tolud_val) {
      LTRACEF("TOLUD Quirk subtracting region [0x%08x, 0x%08x)\n", 0u, tolud_val);
      zx_status_t res = dev->driver().SubtractBusRegion(0u, tolud_val, PciAddrSpace::MMIO);
      if (res != ZX_OK)
        TRACEF(
            "WARNING : PCIe TOLUD Quirk failed to subtract region "
            "[0x%08x, 0x%08x) (res %d)!\n",
            0u, tolud_val, res);
    }
  }

  found_chipset_device = true;
}

static void pcie_amd_topmem_quirk(const fbl::RefPtr<PcieDevice>& dev) {
  // only makes sense on AMD hardware
  if (x86_vendor != X86_VENDOR_AMD)
    return;

  // do this the first time
  static bool initialized = false;
  if (initialized)
    return;

  // only do this once
  initialized = true;

  // see if the TOP_MEM and TOP_MEM2 msrs are active by reading the SYSCFG MSR
  uint64_t syscfg = read_msr(0xc0010010);
  LTRACEF("SYSCFG 0x%lx\n", syscfg);

  // for AMD, use the TOP_MEM and TOP_MEM2 MSR
  // see AMD64 architecture programming manual, volume 2, rev 3.25, page 209
  uint64_t top_mem = 0;
  uint64_t top_mem2 = 0;
  if (syscfg & (1 << 20)) {  // MtrrVarDramEn
    top_mem = read_msr(0xc001001a);
  }
  if (syscfg & (1 << 21)) {  // MtrrTom2En
    top_mem2 = read_msr(0xc001001d);
  }

  /* mask out reserved bits */
  top_mem &= ((1ULL << 52) - 1);
  top_mem &= ~((1ULL << 23) - 1);
  top_mem2 &= ((1ULL << 52) - 1);
  top_mem2 &= ~((1ULL << 23) - 1);

  LTRACEF("TOP_MEM %#" PRIx64 " TOP_MEM2 %#" PRIx64 "\n", top_mem, top_mem2);

  if (top_mem >= UINT32_MAX) {
    TRACEF("WARNING: AMD TOP_MEM >= 4GB\n");
  }

  if (top_mem && dev) {
    zx_status_t res = dev->driver().SubtractBusRegion(0u, top_mem, PciAddrSpace::MMIO);
    if (res != ZX_OK) {
      TRACEF(
          "WARNING : PCIe AMD top_mem quirk failed to subtract region "
          "[0x0, %#" PRIx64 ") (res %d)!\n",
          top_mem, res);
    }
  }

  if (top_mem2 && dev) {
    uint64_t max = (1ULL << x86_physical_address_width());

    // TODO: make this subtractive on (0, TOP_MEM2) when we start preloading the
    // upper pci range.
    zx_status_t res = dev->driver().AddBusRegion(top_mem2, max, PciAddrSpace::MMIO);
    if (res != ZX_OK) {
      TRACEF(
          "WARNING : PCIe AMD top_mem quirk failed to add 64bit region "
          "[%#" PRIx64 ", %#" PRIx64 ") (res %d)!\n",
          top_mem2, max, res);
    }
  }
}

extern const PcieBusDriver::QuirkHandler pcie_quirk_handlers[] = {
    pcie_tolud_quirk,
    pcie_amd_topmem_quirk,
    nullptr,
};

#endif  // WITH_KERNEL_PCIE
